performance.now() が monotonic (単調増加) なことを利用すると、システム時計の変化を比較的高精度に得られるなと思ったので、以下のようなClockMonitorクラスを作ってみた。

// ClockMonitor: システム時計の大幅な変更(NTP補正・手動変更等)を検知し、イベントを発行するクラス
// WebAudioやperformance.now()はmonotonicな経過時間だが、絶対時刻(Date.now())はシステム時計依存でジャンプすることがある
// そのため、performance.timeOrigin+performance.now()で絶対時刻を計算している場合、
// システム時計が変化しても自動で補正されない(ズレたままになる)
// このクラスは、定期的にDate.now()とperformance.timeOrigin+performance.now()の差分を監視し、
// 一定以上の差分が発生した場合に"clockchange"イベントを発行することで、
// 利用側がoffset等を補正できるようにする
class ClockMonitor extends EventTarget {
	constructor({ threshold = 2000, interval = 1000 } = {}) {
		super();
		this.threshold = threshold; // 何ms以上の差分で検知するか
		this.interval = interval;   // 監視間隔(ms)
		this.offset = performance.timeOrigin || 0; // performance.now()の起点(初期化時の絶対時刻)
		this._timer = null;
	}

	start() {
		if (this._timer) return;
		this._timer = setInterval(() => {
			const perfNow = performance.now();
			const now = Date.now();
			// 現在の絶対時刻の期待値(初期offset+経過時間)
			const expected = this.offset + perfNow;
			const diff = now - expected;
			// threshold以上の差分が出たらシステム時計変更とみなす
			if (Math.abs(diff) > this.threshold) {
				// offsetを補正し、イベント発行
				this.offset += diff;
				this.dispatchEvent(new CustomEvent("clockchange", {
					detail: { offset: this.offset, diff }
				}));
			}
		}, this.interval);
	}

	stop() {
		if (this._timer) {
			clearInterval(this._timer);
			this._timer = null;
		}
	}
}

他の方法

Date.now() を単に保持しておいて比較することでも、過去への遡りは検出できる。が、未来へ進むのは検出できない (ただのタイマーの遅れと区別できない)

問題点

問題点: performance.now() は monotonic ではあるがスリープで時間の連続性が失われることがある
仕様上は連続することになっているが、一部の環境のブラウザだけ。

  1. トップ
  2. tech
  3. JSでシステム時計の変化(時刻変更、NTP同期)を検知する

micro-template.js という2012年に作った embed JS 的なテンプレート処理ライブラリがある。コピペできるぐらい小さくて、早いことがコンセプト。

完全に放置してたけど、ちょっと手を入れはじめたらいろいろやりたくなってしまったので、だいぶ改修をいれてしまった。

  1. トップ
  2. tech
  3. micro-template.js を13年ぶりにいろいろいじった

いきなりトランスパイルの環境作って常時ビルドツールを動かして開発するのがいまいち性にあわず、いまだにそういうことをしないようにしてる。小さいプロジェクトだと管理が面倒くさい。

Vue3 はなんかいろいろさらに面倒になっており、公式のクイックスタートが全然クイックじゃねーよと思っていた。さらにクイックな方法を試行錯誤してたけど、現状の最小限サンプルを作っておくことにした。追記: 公式にも最小限サンプルあるわ……

最近のブラウザは ESM の import に対応しており、これ前提なら変なこと(ビルド)しなくても、このままに動く。ただブラウザで開くだけで良い。開発開始には十分だし、余計なことを考える必要はない。

一応ファイルを分けてるけど、app.js の内容は index.html に埋めこんでも良い。

「バニラJSだと面倒くさいUI状態があるが、いっぱいビルドとかはしたくない」みたいなことは多い。

以下は template 要素を使っている。template 要素を div 要素にして createApp に template を指定しなくても、mount("#app") でもいける。けど、template じゃない場合、table の td tr などで要素が消滅してハマることがある。

// app.js
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
createApp({
	data() {
		return {
			counter: 0,
		}
	},
	mounted() {
		console.log('App mounted');
	},
	template: document.querySelector('#app').innerHTML
}).mount(document.body);
<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Hello</title>
	<script type="module" src="./app.js" defer></script>
</head>
<body>
	<template id="app">
		<h1>Hello</h1>
		<button @click="counter++">Click me</button>
		<p>Counter: {{ counter }}</p>
	</template>
</body>
</html>

petite-vue はメンテされてるのだろうか。どこかのタイミングでVue3の機能やらビルドをしたくなったときのことを考えると Vue3 そのまま使ってもいいと思う。

  1. トップ
  2. tech
  3. 一瞬で開発開始するための Vue3 無トランスパイル環境